/*
 * SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
 * SPDX-License-Identifier: GPL-3.0-only
 */

package com.sanji.audio_test.output

import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.system.Int64Ref
import android.system.Os
import android.system.OsConstants
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.documentfile.provider.DocumentFile
import com.sanji.audio_test.Preferences
import com.sanji.audio_test.extension.NotEfficientlyMovableException
import com.sanji.audio_test.extension.createNestedFile
import com.sanji.audio_test.extension.deleteIfEmptyDirRecursively
import com.sanji.audio_test.extension.findNestedFile
import com.sanji.audio_test.extension.findOrCreateDirectories
import com.sanji.audio_test.extension.getExtensionFromMimeTypeCompat
import com.sanji.audio_test.extension.moveToDirectory
import com.sanji.audio_test.extension.renameToPreserveExt
import com.sanji.audio_test.extension.toDocumentFile
import java.io.FileNotFoundException
import java.io.IOException

/**
 * 输出目录工具类
 * 提供文件创建、移动和管理功能
 *
 * @param context 应用上下文
 * @param redactor 用于敏感信息脱敏的接口
 */
class OutputDirUtils(private val context: Context, private val redactor: Redactor) {
    // 应用偏好设置
    private val prefs = Preferences(context)

    /**
     * 获取错误回退路径
     * 当文件操作失败时使用此路径作为备选
     */
    private fun getErrorFallbackPath(path: List<String>) =
        listOf("ERROR_${path.joinToString("_")}")

    /**
     * 获取现有路径的文件
     *
     * @param root 根目录
     * @param path 相对于根目录的路径
     * @return 对应的DocumentFile对象
     * @throws FileNotFoundException 如果文件不存在
     */
    private fun getExistingPath(root: DocumentFile, path: List<String>): DocumentFile {
        return root.findNestedFile(path)
            ?: throw FileNotFoundException("无法在 " + redactor.redact(root.uri) + " 中找到 " +
                    redactor.redact(path))
    }

    /**
     * 获取或创建目录
     *
     * @param root 根目录
     * @param path 相对于根目录的路径
     * @return 对应的DocumentFile对象
     * @throws IOException 如果目录无法创建
     */
    private fun getOrCreateDirectory(root: DocumentFile, path: List<String>): DocumentFile {
        return root.findOrCreateDirectories(path)
            ?: throw IOException("无法在 " + redactor.redact(root.uri) + " 中找到或创建 " +
                    redactor.redact(path))
    }

    /**
     * 在指定目录中创建文件
     *
     * @param root 根目录
     * @param path 相对于根目录的路径
     * @param mimeType 文件MIME类型
     * @return 创建的DocumentFile对象
     * @throws IOException 如果文件创建失败
     */
    private fun createFile(root: DocumentFile, path: List<String>, mimeType: String): DocumentFile {
        require(path.isNotEmpty()) { "路径不能为空" }

        val redactedPath = redactor.redact(path)
        val redactedRoot = redactor.redact(root.uri)
        Log.d(TAG, "在 $redactedRoot 中创建 $redactedPath，MIME类型为 $mimeType")

        return root.createNestedFile(mimeType, path)
            ?: throw IOException("无法在 $redactedRoot 中创建文件 $redactedPath")
    }

    /**
     * 创建文件，失败时使用回退路径
     *
     * @param root 根目录
     * @param path 相对于根目录的路径
     * @param mimeType 文件MIME类型
     * @return 创建的DocumentFile对象
     */
    private fun createFileWithFallback(
        root: DocumentFile,
        path: List<String>,
        mimeType: String,
    ): DocumentFile {
        return try {
            createFile(root, path, mimeType)
        } catch (e: Exception) {
            Log.w(TAG, "创建文件失败；使用回退路径", e)

            // 如果失败是由于空间不足等原因，则无法再次成功。
            // 但如果失败是由于中间路径段是文件而不是目录，则至少用户仍会有录音文件，
            // 即使目录结构不正确。
            createFile(root, getErrorFallbackPath(path), mimeType)
        }
    }

    /**
     * 打开可寻址的文件描述符
     *
     * @param file 要打开的文件
     * @param truncate 是否截断文件
     * @throws IOException 如果文件无法打开
     */
    fun openFile(file: DocumentFile, truncate: Boolean): ParcelFileDescriptor {
        val truncParam = if (truncate) { "t" } else { "" }
        return context.contentResolver.openFileDescriptor(file.uri, "rw$truncParam")
            ?: throw IOException("无法打开文件 ${file.uri}")
    }

    /**
     * 通过复制+删除的方式移动文件
     *
     * @param sourceFile 源文件
     * @param targetFile 目标文件
     * 两个文件都必须已经创建
     */
    private fun copyAndDelete(sourceFile: DocumentFile, targetFile: DocumentFile) {
        try {
            openFile(sourceFile, false).use { sourcePfd ->
                openFile(targetFile, true).use { targetPfd ->
                    // 获取源文件大小
                    var remain = Os.lseek(sourcePfd.fileDescriptor, 0, OsConstants.SEEK_END)
                    val offset = Int64Ref(0)

                    // 循环复制文件内容
                    while (remain > 0) {
                        val ret = Os.sendfile(
                            targetPfd.fileDescriptor, sourcePfd.fileDescriptor, offset, remain)
                        if (ret == 0L) {
                            throw IOException("sendfile()中出现意外的EOF")
                        }

                        remain -= ret
                    }

                    // 同步目标文件
                    Os.fsync(targetPfd.fileDescriptor)
                }
            }

            // 删除源文件
            sourceFile.delete()
        } catch (e: Exception) {
            // 如果出错，删除目标文件并重新抛出异常
            targetFile.delete()
            throw e
        }
    }

    /**
     * 移动文件
     *
     * 如果条件允许，将使用高效方式移动（如file URI的rename()和DocumentsProvider URI的moveDocument()）。
     * 否则通过复制然后删除源文件的方式完成移动。
     *
     * @return 新移动文件的DocumentFile对象
     */
    private fun move(
        sourceTree: DocumentFile,
        sourcePath: List<String>,
        targetTree: DocumentFile,
        targetPath: List<String>,
        mimeType: String,
    ): DocumentFile {
        require(targetPath.isNotEmpty()) { "目标路径不能为空" }

        // 获取源文件
        val sourceFile = getExistingPath(sourceTree, sourcePath)
        // 获取目标父目录路径和父目录
        val targetParentPath = targetPath.dropLast(1)
        val targetParent = getOrCreateDirectory(targetTree, targetParentPath)

        // 尝试高效移动
        try {
            val targetFile = sourceFile.moveToDirectory(targetParent)
            if (targetFile != null) {
                // 检查MIME类型是否应该有扩展名
                val hasExt = !MimeTypeMap.getSingleton()
                    .getExtensionFromMimeTypeCompat(mimeType)
                    .isNullOrEmpty()

                // 我们的行为类似于SAF，目标路径不包含文件扩展名，
                // 但查询当前文件名时包含扩展名。只有当应该有扩展名时才截断扩展名进行比较。
                val (oldFilename, renameFunc) = if (hasExt) {
                    Pair(
                        targetFile.name!!.substringBeforeLast('.'),
                        DocumentFile::renameToPreserveExt,
                    )
                } else {
                    Pair(
                        targetFile.name!!,
                        DocumentFile::renameTo,
                    )
                }
                val newFilename = targetPath.last()

                // 如果文件名不同，则重命名
                if (oldFilename != newFilename && !renameFunc(targetFile, newFilename)) {
                    // 我们故意不报告此错误，以便用户可以看到有效的但命名不正确的目标文件，
                    // 而不是现在不存在的源文件。
                    Log.w(TAG, "无法将目标文件从 " +
                            redactor.redact(oldFilename) + " 重命名为 "  + redactor.redact(newFilename))
                }

                return targetFile
            } else {
                Log.w(TAG, "无法高效地将 ${redactor.redact(sourceFile.uri)} 移动到 " +
                        redactor.redact(targetParent.uri))
            }
        } catch (e: NotEfficientlyMovableException) {
            // 故意省略堆栈跟踪
            Log.w(TAG, "${redactor.redact(sourceFile.uri)} 无法高效地移动到 " +
                    "${redactor.redact(targetParent.uri)}: ${e.message}")
        }

        // 如果高效移动失败，则使用复制+删除方式
        val targetFile = createFile(targetTree, targetPath, mimeType)
        copyAndDelete(sourceFile, targetFile)
        return targetFile
    }

    /**
     * 在默认输出目录中创建文件
     *
     * @param path 路径，最后一个元素是文件名，不应包含文件扩展名
     * @param mimeType 确定文件扩展名
     *
     * @throws IOException 如果无法在默认目录中创建文件
     */
    fun createFileInDefaultDir(path: List<String>, mimeType: String): DocumentFile {
        val defaultDir = DocumentFile.fromFile(prefs.defaultOutputDir)

        return createFileWithFallback(defaultDir, path, mimeType)
    }

    /**
     * 尝试将源文件移动到用户输出目录的指定路径
     *
     * @param sourceFile 源文件
     * @param path 目标路径
     * @param mimeType 文件MIME类型
     * @return 如果用户输出目录已设置且文件成功移动则返回目标文件，否则返回null
     */
    fun tryMoveToOutputDir(
        sourceFile: DocumentFile,
        path: List<String>,
        mimeType: String,
    ): DocumentFile? {
        val userDir = prefs.outputDirOrDefault.toDocumentFile(context)
        val redactedSource = redactor.redact(sourceFile.uri)

        return try {
            val targetFile = try {
                move(sourceFile, emptyList(), userDir, path, mimeType)
            } catch (e: Exception) {
                Log.w(TAG, "移动文件失败；使用回退路径", e)
                move(sourceFile, emptyList(), userDir, getErrorFallbackPath(path), mimeType)
            }
            val redactedTarget = redactor.redact(targetFile.uri)

            Log.i(TAG, "成功将 $redactedSource 移动到 $redactedTarget")

            try {
                // 尝试删除空的源目录
                sourceFile.parentFile?.deleteIfEmptyDirRecursively()
            } catch (e: Exception) {
                Log.w(TAG, "清理空源目录失败", e)
            }

            targetFile
        } catch (e: Exception) {
            Log.e(TAG, "将 $redactedSource 移动到 $userDir 失败", e)
            null
        }
    }

    companion object {
        private val TAG = OutputDirUtils::class.java.simpleName

        // 空脱敏器，不进行任何脱敏处理
        val NULL_REDACTOR = object : Redactor {
            override fun redact(msg: String): String = msg
        }
    }

    /**
     * 脱敏接口，用于对敏感信息进行脱敏处理
     */
    interface Redactor {
        /**
         * 对消息进行脱敏
         */
        fun redact(msg: String): String

        /**
         * 对URI进行脱敏
         */
        fun redact(uri: Uri): String = redact(Uri.decode(uri.toString()))

        /**
         * 对路径进行脱敏
         */
        fun redact(path: List<String>): String = redact(path.joinToString("/"))
    }
}